package org.openstreetmap.josm.plugins.cadastre_es;

import java.util.Vector;

import org.openstreetmap.josm.plugins.cadastre_es.exceptions.NotClosedPolygon;
import org.openstreetmap.josm.plugins.cadastre_es.exceptions.NotPolygon;

public class Polygon {
	private Vector<PolyPixel> vertexes = new Vector<PolyPixel>();
	private Vector<Polygon> holes = new Vector<Polygon>();
	private PolyPixel start, entry;
	private Pixel.direction dir1;
	private Pixel.direction dir2;
	private Boolean limitPolygon = false;
	private int id;
	private boolean clockwise = false;
	public static int buildings = 0;
    private int numBorderPixels = 0;
	private static final float MAX_DIF_ANGLE = 0.12f;
	private static final int MAX_BORDER_PIXELS = 8000;
	
	public Polygon(Pixel p, boolean isHole) throws NotPolygon, NotClosedPolygon {
		if (!isBuilding(p.right()))
			throw new NotPolygon("Too tiny building polygon, entry pixel: x=" + p.x() + "; y=" + p.y());
		else if (isHole && !isHole(p.right()))
			throw new NotPolygon("Too tiny hole polygon, entry pixel: x=" + p.x() + "; y=" + p.y());
		if (isEndPixel(p))
			throw new NotPolygon("Start is end pixel, entry pixel: x=" + p.x() + "; y=" + p.y());
			//clockwise = true;
		start = new PolyPixel(p);
		entry = start;
		PolyPixel next;
		if (!isHole)
			next = new PolyPixel(getNextStarterPixel(start));
		else
			next = new PolyPixel(getNextHolePixel(start));
		
		if (next == null)
			throw new NotPolygon("Error: Next to start not found, entry pixel: x=" + start.x() + "; y=" + start.y());
		
		start.setRGB(Pixel.FOLLOWED);
	
		start.setNext(next);
		next.setPrev(start);
		
		dir1 = start.getDirection(next);
		dir2 = Pixel.direction.UNDEFINED;
		if (start.inLimit() || next.inLimit())
			limitPolygon = true;
		
		rNextPixel(start, next);
		
		if (!limitPolygon) {
			improve();
			removeUselessVertexes();
			buildings++;
		}
		else {
		    emptyPolygon(entry.right());
			throw new NotPolygon("Border building: " + vertexes.size() + " vertexes, entry pixel: x=" + start.x() + "; y=" + start.y());
		}
		
		if (vertexes.size() < 3)
			throw new NotPolygon("Polygon with only " + vertexes.size() + " vertexes! entry pixel: x=" + start.x() + "; y=" + start.y());

		if (!isHole)
			emptyPolygon(entry.right());
		
		id = ++Process.id;
		
		System.out.println("New building! " + vertexes.size() + " vertexes, entry pixel: x=" + start.x() + "; y=" + start.y() + ", id=" + id);
	}
	
	public Vector<PolyPixel> getVertexes() {
		return vertexes;
	}
	
	public Vector<Polygon> getHoles() {
		return holes;
	}
	
	public PolyPixel getStart() {
		return start;
	}
	
	public int getId() {
		return id;
	}
	
	private Pixel getNextStarterPixel(Pixel p) throws NotPolygon {
		if (p.bottomRight().isBorder())
			return p.bottomRight();
		else if (p.bottom().isBorder())
			return p.bottom();
		else if (p.bottomLeft().isBorder())
			return p.bottomLeft();
		else if (p.left().isBorder())
			return p.left();
		else
			throw new NotPolygon("Error: getStarterPixel pixel: ("+p.x()+","+p.y()+"), entry pixel: x=" + start.x() + "; y=" + start.y());
	}
	
	private void rNextPixel(PolyPixel vertex, PolyPixel current) throws NotClosedPolygon, NotPolygon {
		if (++numBorderPixels > MAX_BORDER_PIXELS) {
		    emptyBadPolygon(start.right());
		    throw new NotPolygon("Error: too big polygon, pixel: ("+current.x()+","+current.y()+"), entry pixel: x=" + start.x() + "; y=" + start.y());
		}
		    
		if (current.outside()) {
			throw new NotPolygon("rNextPixel: Outside");
		}
		current.setRGB(Pixel.FOLLOWED);
		
		PolyPixel next = getNext(current.getPrev(), current);
		
		if (!limitPolygon && next.inLimit())
			limitPolygon = true;
		
		next.setPrev(current);
		current.setNext(next);
		
		if (next.equal(start)) {
			if (vertexes.size() == 0)
				throw new NotPolygon("Zero vertexes, entry pixel: x=" + start.x() + "; y=" + start.y());
			if (new IsVertex(vertexes.lastElement(), vertexes.firstElement()).getRvalue()) {
				// Entry point (start) is vertex
				start.setVertex(true);
				vertexes.insertElementAt(start, 0);
			}
			start.setPrev(current);
			current.setNext(start);
			return;
		}
		
		Pixel.direction cDirection = current.getDirection(next);
		if (dir2 == Pixel.direction.UNDEFINED && dir1 != cDirection)
			dir2 = cDirection;
		
		Boolean fixedVertex = cDirection != dir1 && cDirection != dir2;
		if (fixedVertex)
			current.setFixedVertex(true);
		
		if (fixedVertex || (current.distance(vertex) > 1 && new IsVertex(vertex, next).getRvalue())) {
			// Vertex found
			current.setVertex(true);
			vertexes.add(current);
			dir1 = current.getDirection(next);
			dir2 = Pixel.direction.UNDEFINED;
			rNextPixel(current, next);						
		}
		else 
			rNextPixel(vertex, next);
	}
	
	/*private void reverse(PolyPixel p) throws NotPolygon, NotClosedPolygon {
		clockwise = true;
		
		while (p.betterDistance(p.getPrev()) != 1)
			p = p.getPrev();
		
		start = new PolyPixel(p);
		PolyPixel next = new PolyPixel(p.getPrev());
		vertexes.clear();
		
		start.setNext(next);
		next.setPrev(start);
		next.setVertex(false);
		dir1 = start.getDirection(next);
		dir2 = direction.UNDEFINED;
		if (start.inLimit() || next.inLimit())
			limitPolygon = true;
		
		rNextPixel(start, next);
	}*/
	
	public void improve() {
		int numVertexes = vertexes.size();
		for (int vnum = 0; vnum < numVertexes; vnum++) {
			int beforeIndex = vnum - 1;
			if (beforeIndex == -1)
				beforeIndex = numVertexes - 1; 
			int afterIndex = (vnum + 1) % numVertexes;
			PolyPixel before = vertexes.get(beforeIndex);
			PolyPixel after = vertexes.get(afterIndex);
			PolyPixel current = vertexes.get(vnum);
			PolyPixel vMove;
			PolyPixel vBest;
			
			//current.setRGB(0xFFFF00FF);
			
			// Don't improve fixed vertexes
			//if (current.getFixedVertex())
			//	continue;
			
			// Improve vertex
			int bestMark = (new GetMark(before, current).getRvalue() + new GetMark(after, current).getRvalue()) / 2;
			int actualMark = bestMark;
			vMove = current.getPrev();
			vBest = current;
			while (!vMove.equal(before)) {
				actualMark = (new GetMark(before, vMove).getRvalue() + new GetMark(after, vMove).getRvalue()) / 2;
				if (actualMark > bestMark) {
					bestMark = actualMark;
					vBest = vMove;
				}
				else if (actualMark < 0)
					break;
				vMove = vMove.getPrev();
			}
			vMove = current.getNext();
			while (!vMove.equal(after)) {
				actualMark = (new GetMark(before, vMove).getRvalue() + new GetMark(after, vMove).getRvalue()) / 2;
				if (actualMark > bestMark) {
					bestMark = actualMark;
					vBest = vMove;
				}
				else if (actualMark < 0)
					break;
				vMove = vMove.getNext();
			}
			if (vBest != current) {
				if (vBest.equal(before) || vBest.equal(after)) {
					vertexes.remove(vnum);
					current.setVertex(false);
					numVertexes--;
					vnum--;
				}
				else {
					// Found an improved vertex!
					current.setVertex(false);
					vBest.setVertex(true);
					vertexes.setElementAt(vBest, vnum);
				}
			}
		}
	}
	
	public void removeUselessVertexes() {
		int numVertexes = vertexes.size();
		for (int vnum = 0; vnum < numVertexes; vnum++) {
			int beforeIndex = vnum - 1;
			if (beforeIndex == -1)
				beforeIndex = numVertexes - 1; 
			int afterIndex = (vnum + 1) % numVertexes;
			PolyPixel before = vertexes.get(beforeIndex);
			PolyPixel after = vertexes.get(afterIndex);
			PolyPixel current = vertexes.get(vnum);
			
			// Join closer vertexes
            /*if (current.distance(after) < 4) {
                PolyPixel mCurrent = current;
                PolyPixel mAfter = after;
                Boolean currentMoved = false;
                while (!mCurrent.equal(mAfter)) {
                    if (!currentMoved) {
                        mCurrent = mCurrent.getNext();
                        currentMoved = true;
                    }
                    else { 
                        mAfter = mAfter.getPrev();
                        currentMoved = false;
                    }
                }
                if (current == mCurrent) {
                    after.setVertex(false);
                    vertexes.remove(afterIndex);
                }
                else if (after == mCurrent) {
                    current.setVertex(false);
                    vertexes.remove(vnum);
                }
                else {
                	current.setVertex(false);
                    mCurrent.setVertex(true);
                    vertexes.setElementAt(mCurrent, vnum);
                    after.setVertex(false);
                    vertexes.remove(afterIndex);
                }
                vnum--;
                numVertexes--;
                continue;
            }*/
			
			float beforeAngle = before.angle(current);
			float afterAngle = current.angle(after);
			float dAngle = Math.abs(beforeAngle - afterAngle);
			
			if ((dAngle < MAX_DIF_ANGLE) || (dAngle > (2*Math.PI - MAX_DIF_ANGLE))) {
				current.setVertex(false);
				vertexes.remove(vnum);
				vnum--;
				numVertexes--;
				continue;
			}
		}
		
	}
	
	private void emptyPolygon(Pixel p) {
		Vector<Pixel> pixels = new Vector<Pixel>();
		pixels.add(p);
		while (pixels.size() > 0) {
			Pixel c = pixels.remove(pixels.size()-1);
			c.setRGB(Pixel.FBUILDING);
			if (c.left().isBuilding())
				pixels.add(c.left());
			else if(c.left().isNewBorder()) {
				if (isBorderHole(c.left()))
					getHole(c.left());
				else
					pixels.add(c.left());
			}
			if (c.bottom().isBuilding())
				pixels.add(c.bottom());
			else if(c.bottom().isNewBorder()) {
				if (isBorderHole(c.bottom()))
					getHole(c.bottom());
				else
					pixels.add(c.bottom());
			}
			if (c.top().isBuilding())
				pixels.add(c.top());
			else if(c.top().isNewBorder()) {
				if (isBorderHole(c.top()))
					getHole(c.top());
				else
					pixels.add(c.top());
			}
			if (c.right().isBuilding())
				pixels.add(c.right());
			else if(c.right().isNewBorder()) {
				if (isBorderHole(c.right())) 
					getHole(c.right());
				else
					pixels.add(c.right());
			}
		}
	}
	
	private void emptyBadPolygon(Pixel p) {
        Vector<Pixel> pixels = new Vector<Pixel>();
        pixels.add(p);
        while (pixels.size() > 0) {
            Pixel c = pixels.remove(pixels.size()-1);
            c.setRGB(Pixel.WHITE);
            if (c.left().isBuilding())
                pixels.add(c.left());
            if (c.bottom().isBuilding())
                pixels.add(c.bottom());
            if (c.top().isBuilding())
                pixels.add(c.top());
            if (c.right().isBuilding())
                pixels.add(c.right());
        }
    }
	
	private boolean isBuilding(Pixel p) {
		Vector<Pixel> pixels = new Vector<Pixel>();
		Vector<Pixel> scanedPixels = new Vector<Pixel>();
		int counter = 0;
		pixels.add(p);
		while (pixels.size() > 0) {
			if (++counter > 5)
				return true;
			Pixel c = pixels.remove(pixels.size()-1);
			scanedPixels.add(c);
			
			if (c.top().isBuilding() && !scanedPixels.contains(c.top()))
				pixels.add(c.top());
			if (c.bottom().isBuilding() && !scanedPixels.contains(c.bottom()))
				pixels.add(c.bottom());
			if (c.left().isBuilding() && !scanedPixels.contains(c.left()))
				pixels.add(c.left());
			if (c.right().isBuilding() && !scanedPixels.contains(c.right()))
				pixels.add(c.right());
		}
		return false;
	}
	
	private boolean isHole(Pixel p) {
		Vector<Pixel> pixels = new Vector<Pixel>();
		int counter = 0;
		pixels.add(p);
		while (pixels.size() > 0) {
			if (++counter > 5)
				return true;
			Pixel c = pixels.remove(pixels.size()-1);
			
			if (c.top().isHole())
				pixels.add(c.top());
			if (c.bottom().isHole())
				pixels.add(c.bottom());
			if (c.left().isHole())
				pixels.add(c.left());
			if (c.right().isHole())
				pixels.add(c.right());
		}
		return false;
	}
	
	private Pixel getNextHolePixel(Pixel p) {
		if (p.right().isHole()) {
			if (p.bottomRight().isBorder())
				return p.bottomRight();
			if (p.bottom().isBorder())
				return p.bottom();
			if (p.bottomLeft().isBorder())
				return p.bottomLeft();
		}
		if (p.left().isHole()) {
			if (p.topLeft().isBorder())
				return p.topLeft();
			if (p.top().isBorder())
				return p.top();
			if (p.topRight().isBorder())
				return p.topRight();
		}
		return null;
	}
	
	private void getHole(Pixel p) {
		if (isBadStartPixel(p))
			return;
		try {
			Polygon polygon = new Polygon(p, true);
			holes.add(polygon);
		}
		catch(NotPolygon e) {	}
		catch(NotClosedPolygon e) {
			System.out.println(e.getMessage());
		}
	}
	
	private boolean isBorderHole(Pixel p) {
		int whitePixels = 0;
		
		if (p.top().isWhiteNonFBuilding())
			whitePixels++;
		if (p.topRight().isWhiteNonFBuilding())
			whitePixels++;
		if (p.right().isWhiteNonFBuilding())
			whitePixels++;
		if (p.bottomRight().isWhiteNonFBuilding())
			whitePixels++;
		if (p.bottom().isWhiteNonFBuilding())
			whitePixels++;
		if (p.bottomLeft().isWhiteNonFBuilding())
			whitePixels++;
		if (p.left().isWhiteNonFBuilding())
			whitePixels++;
		if (p.topLeft().isWhiteNonFBuilding())
			whitePixels++;
		
		if (whitePixels > 0)
			return true;
		return false;
	}
	
	private boolean isEndPixel(Pixel p) {
		return countNearBorderPixels(p) < 3;
	}
	
	private boolean isBadStartPixel(Pixel p) {
		return countNearBorderPixels(p) > 3;
	}
	
	private int countNearBorderPixels(Pixel p) {
		int borderPixels = 1;
		
		if (p.top().isBorder())
			borderPixels++;
		if (p.topRight().isBorder())
			borderPixels++;
		if (p.right().isBorder())
			borderPixels++;
		if (p.bottomRight().isBorder())
			borderPixels++;
		if (p.bottom().isBorder())
			borderPixels++;
		if (p.bottomLeft().isBorder())
			borderPixels++;
		if (p.left().isBorder())
			borderPixels++;
		if (p.topLeft().isBorder())
			borderPixels++;
		
		return borderPixels;
	}
	
	private PolyPixel getNext(PolyPixel prev, PolyPixel current) throws NotClosedPolygon {
		Pixel next = new Pixel(prev);
		if (current.x() == 1589 && current.y()==273)
			System.out.print("");
		
		boolean found = false;
		for (int pnum = 1; pnum < 7; pnum++) {
			next = calculeNext(current, next, false);
			if (next.isBorder()) {
				found = true;
				break;
			}
		}
		if (!found) {
			next = new Pixel(prev.getPrev());
			next = calculeNext(current, next, true);
			found = false;
			for (int pnum=1; pnum<14; pnum++) {
				next = calculeNext(current, next, true);
				if (next == null)
					throw new NotClosedPolygon("WARNING (b): Not closed polygon found! entry point: " + start.x() + ", "+start.y() + "; current: " + current.x() + ", " + current.y( ));
				if (next.isBorder()) {
					found = true;
					break;
				}
			}
			if (!found)
				throw new NotClosedPolygon("WARNING: Not closed polygon found! entry point: " + start.x() + ", " + start.y() + "; current: " + current.x() + ", " + current.y());
			else {
				int difx = current.x() - next.x();
				int dify = current.y() - next.y();
				if ((difx == -2 && dify == -1) || (difx == -2 && dify == -2) || (difx == -1 && dify == -2))
					next = current.bottomRight();
				else if((difx == 2 && dify == -1) || (difx == 2 && dify == -2) || (difx == 1 && dify == -2))
					next = current.bottomLeft();
				else if((difx == 2 && dify == 1) || (difx == 2 && dify == 2) || (difx == 1 && dify == 2))
					next = current.topLeft();
				else if((difx == -2 && dify == 1) || (difx == -2 && dify == 2) || (difx == -1 && dify == 2))
					next = current.topRight();
				else if(difx == 0 && dify == 2)
					next = current.top();
				else if(difx == -2 && dify == 0)
					next = current.right();
				else if(difx == 0 && dify == -2)
					next = current.bottom();
				else if(difx == 2 && dify == 0)
					next = current.left();
				next.setRGB(Pixel.BUILDING);
			}
		}
		return new PolyPixel(next);
	}
	
	private Pixel calculeNext(Pixel current, Pixel next, boolean far) {
		if (next == null)
			return null;
		int difx = current.x() - next.x();
		int dify = current.y() - next.y();
		
		if (!far) {
			if ((Math.abs(next.x() - current.x()) < 2) && (Math.abs(next.y() - current.y()) < 2)) {
				if (!clockwise) {
					if ((difx == 1 && dify == 1) || (difx == 0 && dify == 1))
						next = next.right();
					else if ((difx == -1 && dify == 1) || (difx == -1 && dify == 0))
						next = next.bottom();
					else if ((difx == -1 && dify == -1) || (difx == 0 && dify == -1))
						next = next.left();
					else if ((difx == 1 && dify == -1) || (difx == 1 && dify == 0))
						next = next.top();
				}
				else {
					if ((difx == 1 && dify == 1) || (difx == 1 && dify == 0))
						next = next.bottom();
					else if ((difx == 1 && dify == -1) || (difx == 0 && dify == -1))
						next = next.right();
					else if ((difx == -1 && dify == -1) || (difx == -1 && dify == 0))
						next = next.top();
					else if ((difx == -1 && dify == 1) || (difx == 0 && dify == 1))
						next = next.left();
				}
			}
			else
				next = null;
		}
		else {
			if (!(Math.abs(difx) > 2 || Math.abs(dify) > 2 || (Math.abs(difx) < 2 && Math.abs(dify) < 2))) {
				if ((difx == 2 && dify == 2) || (difx == 1 && dify == 2) || (difx == 0 && dify == 2) || (difx == -1 && dify == 2))
					next = next.right();
				else if ((difx == -2 && dify == 2) || (difx == -2 && dify == 1) || (difx == -2 && dify == 0) || (difx == -2 && dify == -1))
					next = next.bottom();
				else if ((difx == -2 && dify == -2) || (difx == -1 && dify == -2) || (difx == 0 && dify == -2) || (difx == 1 && dify == -2))
					next = next.left();
				else if ((difx == 2 && dify == -2) || (difx == 2 && dify == -1) || (difx == 2 && dify == 0) || (difx == 2 && dify == 1))
					next = next.top();
			}
			else
				next =  null;
		}
		return next;
	}
}
